package javango.forms;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javango.forms.fields.BooleanField;
import javango.forms.fields.BoundField;
import javango.forms.fields.FieldFactory;
import javango.forms.fields.IntegerField;
import javango.forms.widgets.HiddenInputWidget;

public class FormSet<T extends Form> implements Collection<T> {

	// TODO make this changable.
	private final String formCountFieldName = "TOTAL_FORMS";
	private final String initialFormCountFieldName = "INITIAL_FORMS";
	private final String deletionFieldName = "DELETE";
	
	protected final Forms forms;
	protected final FieldFactory fieldFactory;
	protected final Class<? extends T> formClass;
	protected final String prefix;
	protected final Integer extra;
	
	protected final List<T> formList = new ArrayList<T>();
	protected final Form controlForm;

	protected final List<?> initial;
	protected final Map<String, String[]> data;
	
	protected final boolean canDelete;
	
	private int getTotalFormCount() throws ValidationException {
		if (this.data != null) {
			Integer count = (Integer)controlForm.getCleanedData().get(formCountFieldName);
			if (count == null) {
				throw new ValidationException("ManagementForm data is missing or has been tampered with" );
			}
			return count;
		} else {
			return getInitialFormCount() + extra;
		}
	}
	
	private int getInitialFormCount() throws ValidationException {
		if (this.data != null) {
			Integer count = (Integer)controlForm.getCleanedData().get(initialFormCountFieldName);
			if (count == null) {
				throw new ValidationException("ManagementForm data is missing or has been tampered with" );
			}
			return count;
		} else {
			return initial == null ? 0 : initial.size();
		}
	}
	
	public FormSet(
			Forms forms,
			FieldFactory fieldFactory,
			Class<? extends T> formClass,
			boolean canDelete,
			List<?> initial,
			Map<String, String[]> data,
			String prefix, 
			Integer extra) throws ValidationException {
		this.forms = forms;
		this.fieldFactory = fieldFactory;
		this.formClass = formClass;
		this.canDelete = canDelete;
		
		this.prefix = prefix == null ? "form" : prefix;
		this.extra = extra == null ? 1 : extra;
		this.initial = initial;
		this.data = data;
		
		controlForm = createControlForm();
		
		for(int i=0; i<getTotalFormCount(); i++) {
			formList.add(constructForm(i));
		}
	}
	
	/**
	 * Experimental method which clones this formset into a new formset,  the new formset can optionally have deleted forms removed.
	 * 
	 * The resulting formset's forms initial data are set to the values of their cleanedData and are not bound.  The new formset is 
	 * really only useful for displaying in a template.    
	 * 
	 * @param includeDeleted Should deleted forms be included in the new FormSet
	 * @return
	 * @throws ValidationException 
	 */
	public FormSet<T> clone(boolean includeDeleted) throws ValidationException {
		List<Map<String, Object>> newData = new ArrayList<Map<String,Object>>();
		for(T form : formList) {
			if (includeDeleted || !shouldDeleteForm(form)) {
				newData.add(form.getCleanedData());
			}
		}
		
		return new FormSet<T>(forms, fieldFactory, formClass, canDelete, newData, null, prefix, 0);
	}
	
	/**
	 * Add any exta fields that are needed (ie delete)
	 * @param form
	 */
	private void addExtraFields(T form) {
		if (canDelete) {
			final BooleanField deleteField = fieldFactory.newField(BooleanField.class);
			deleteField.setName(deletionFieldName)
				.setRequired(false)			
				.setAllowNull(false);
			form.getFields().put(deletionFieldName, deleteField);
		}
	}
	
	private T constructForm(int i) {
		Object initialValues = initial == null ? null : i < initial.size() ? initial.get(i) : null;
		T form = forms.newForm(formClass);
		String formPrefix = String.format("%s-%d", this.prefix, i++);
		form.setPrefix(formPrefix);
		form.setInitial(initialValues);
		form.bind(data);
		
		addExtraFields(form);
		
		return form;
	}

	private Form createControlForm() throws ValidationException {
		Form controlForm = forms.newForm(AbstractForm.class);
		controlForm.setPrefix(String.format("%s_CTRL", prefix));
		final IntegerField countField = fieldFactory.newField(IntegerField.class);
		countField.setName(formCountFieldName)
			.setWidget(HiddenInputWidget.class)
			.setRequired(true)			
			.setAllowNull(false);
		controlForm.getFields().put(formCountFieldName, countField);
		
		final IntegerField initialFormCount = fieldFactory.newField(IntegerField.class);
		initialFormCount.setName(initialFormCountFieldName)
			.setWidget(HiddenInputWidget.class)
			.setRequired(true)			
			.setAllowNull(false);
		controlForm.getFields().put(initialFormCountFieldName, initialFormCount);
		
		if (data != null) {
			controlForm.bind(data);
			if (!controlForm.isValid()) {
				throw new UnsupportedOperationException("Missing control fields");
			}
		} else {
			controlForm.getInitial().put(formCountFieldName, getTotalFormCount());
			controlForm.getInitial().put(initialFormCountFieldName, getInitialFormCount());
		}
		return controlForm;
	}

	private boolean shouldDeleteForm(T form) {
		if (!form.isBound()) {
			return false;
		}
		BooleanField field = (BooleanField) form.getFields().get(deletionFieldName);
		String[] value = form.getData().get(String.format("%s-%s", form.getPrefix(), deletionFieldName));
		if (value == null || value.length == 0) {
			return false;
		}
		try {
			return field.clean(value, new HashMap<String, String>());
		} catch (ValidationException e) {
			return false;
		}
	}
	
	/**
	 * Returns true is all forms are valid,  does not short circuit, isValid will be called on all forms.
	 * @return
	 */
	public boolean isValid() {
		boolean isvalid = true;
		for (T form : formList) {
			if (!shouldDeleteForm(form)) {
				if (!form.isValid()) isvalid = false;
			}
		}
		return isvalid;
	}
	
	/**
	 * Returns the HTML for the management form fields.
	 * @return
	 */
	public String getManagementForm() {
		StringBuilder builder = new StringBuilder();
		for(BoundField bf: controlForm) {
			builder.append(bf.toString());
		}
		return builder.toString();
	}
		

	List<T> nonDeletedForms;
	public List<T> getNonDeletedForms() {
		if (nonDeletedForms == null) {
			nonDeletedForms = new ArrayList<T>();
			for(T form : formList) {
				if (!shouldDeleteForm(form)) {
					nonDeletedForms.add(form);
				}
			}
		}
		return nonDeletedForms;

	}
	
	List<T> deletedForms;
	public List<T> getDeletedForms() {
		if (deletedForms == null) {
			deletedForms = new ArrayList<T>();
			for(T form : formList) {
				if (shouldDeleteForm(form)) {
					deletedForms.add(form);
				}
			}
		}
		return deletedForms;
	}
	
	public Iterator<T> iterator() {
		return formList.iterator();
	}

	/**
	 * Return an empty form with a custom prefix for use in javascript
	 */
	public T getEmptyForm(String prefix) {
		T form = forms.newForm(formClass);
		String formPrefix = String.format("%s-%s", this.prefix, prefix);
		form.setPrefix(formPrefix);
		addExtraFields(form);
		return form;
	}
	
	/**
	 * Return an empty form with a prefix of "__prefix__" for use in javascript
	 */
	public T getEmptyForm() {
		return getEmptyForm("__prefix__");
	}

	
	// collectin interface crap...
	public boolean add(T arg0) {
		throw new UnsupportedOperationException();
	}

	public boolean addAll(Collection<? extends T> arg0) {
		throw new UnsupportedOperationException();
	}

	public void clear() {
		throw new UnsupportedOperationException();
	}

	public boolean contains(Object arg0) {
		return formList.contains(arg0);
	}

	public boolean containsAll(Collection<?> arg0) {
		return formList.containsAll(arg0);
	}

	public boolean isEmpty() {
		return formList.isEmpty();
	}

	public boolean remove(Object arg0) {
		throw new UnsupportedOperationException();

	}

	public boolean removeAll(Collection<?> arg0) {
		throw new UnsupportedOperationException();

	}

	public boolean retainAll(Collection<?> arg0) {
		throw new UnsupportedOperationException();
	}

	public int size() {
		return formList.size();
	}

	public Object[] toArray() {
		return formList.toArray();
	}

	public <T> T[] toArray(T[] arg0) {
		return formList.toArray(arg0);
	}

}
